package filereaders;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Pattern;


/**
 * A parser capable of reading from a Scanner or
 * directly from a file.
 * 
 * @author Paul Dannenberg
 * 
 */

public abstract class Parser {

    private Scanner myScanner = null;
    private File myFile = null;

    /**
     * Takes the file that this parser should
     * read from.
     * 
     * @param file
     */
    public Parser(File file) {
        myFile = file;
    }

    /**
     * Takes a scanner that this parser should
     * read from.
     * 
     * @param scanner
     */
    public Parser(Scanner scanner) {
        myScanner = scanner;
    }

    /**
     * 
     * @return A scanner for the particular file
     *         that this Parser can read.
     */

    public Scanner loadScanner() {

        if (myScanner == null || !myScanner.hasNextLine()) {
            try {
                myScanner = new Scanner(myFile);
                return myScanner;
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
        return myScanner;
    }

    /**
     * 
     * @return The options available that
     *         the parser can return data for.
     */
    public abstract List<String> getOptions();

    /**
     * Reads out a particular sector of the text file, e.g. a row or a column.
     * 
     * @param indexOfData A specifier for the sector to read. For example if
     *        the sectors are columns, column 0 may specify the first column,
     *        column 1,
     *        the next etc.
     * @return A container of Doubles corresponding to the index specified by
     *         indexOfData.
     */
    protected abstract List<String> readSector(int indexOfData);

    /**
     * Reads out the data corresponding to a particular field. The list of
     * possible fields can be obtained by the user by calling getOptions().
     * 
     * @param fieldName The string corresponding to the data that should be
     *        read. For example, "Open", or "High" may correspond to the opening
     *        or high daily stock values.
     * @return An object containing the requested data.
     */
    public List<Double> parseDataField(String fieldName) {
        int indexOfData = findFieldIndex(fieldName);
        return format(readSector(indexOfData));
    }

    private int findFieldIndex(String fieldName) {
        return getOptions().indexOf(fieldName);
    }

    /**
     * Returns a map between the data of two fields. This map will always be
     * one-to-one. For example, if the the user requests "date" as the
     * keyField and "volume" as the valueField, this method would return a
     * mapping of dates onto stock volume.
     * 
     * @param keyField The first field to request data for.
     * @param valueField The second field to request data for.
     * @return
     */
    public Map<Double, Double> parseRelatedFields(String keyField,
            String valueField) {
        List<Double> keyData = parseDataField(keyField);
        List<Double> valueData = parseDataField(valueField);
        return merge(keyData, valueData);
    }

    /**
     * 
     * @return An object containing all data stored
     *         in the file. The map will take each data title as
     *         a key and a list of all its data as elements in a
     *         list.
     */
    public Map<String, List<Double>> parseAll() {
        Map<String, List<Double>> parsed = new HashMap<String, List<Double>>();
        for (String option : getOptions()) {
            parsed.put(option, parseDataField(option));
        }
        return parsed;
    }

    /**
     * 
     * @return true if the Parser can parse it's file. False otherwise.
     */
    public boolean canParseFile() {
        return (parseAll() != null);
    }

    private boolean isInteger(String input) {
        return Pattern.matches("^[0-9]+$", input);
    }

    /**
     * Given a List of Strings, this method attempts to convert each
     * element into a Double if possible. If it cannot do this, the
     * original String is not included in returned List.
     * 
     * @param toFormat The List to be converted.
     * @return The formatted List, with values changed from String to
     *         Double.
     */
    protected List<Double> format(List<String> toFormat) {
        List<Double> formatted = new ArrayList<Double>();
        for (String s : toFormat) {
            String formattedString = removeNonNumerics(s);
            if (!("".equals(formattedString) || ".".equals(formattedString))) {
                formatted.add(Double.parseDouble(formattedString));
            }
        }
        return formatted;
    }

    private String removeNonNumerics(String s) {
        String newString = "";
        for (int i = 0; i < s.length(); i++) {
            String letter = Character.toString(s.charAt(i));
            if (isInteger(letter) || ".".equals(letter)) {
                newString += letter;
            }
        }
        return newString;
    }

    /**
     * Merges two data fields together to create a mapping between the
     * two.
     * 
     * @param keyData The first set of data that will form the keys of
     *        the returned map.
     * @param valueData The second set of data that will form the values
     *        of the returned map.
     * @return
     */
    protected <K, V> Map<K, V> merge(List<K> keyData, List<V> valueData) {
        Map<K, V> relatedFields = new HashMap<K, V>();
        for (int i = 0; i < keyData.size(); i++) {
            relatedFields.put(keyData.get(i), valueData.get(i));
        }
        return relatedFields;
    }
}
